﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Globalization;
using Rensoft.Hosting.Server.Data;
using System.Diagnostics;

namespace Rensoft.Hosting.Server.Managers
{
    public class IscBindManager : RhspManager
    {
        private const string zoneConfigFileDirectory = "zones";
        private const string zoneConfigFileNameFormat = "{0}.conf";
        private const string generatedConfigFileComment =
            "This file is generated by Rensoft Hosting 2008 and should not be edited manually.";

        public void CreateZone(DnsZone dnsZone)
        {
            regenerateZoneConfigFile(dnsZone);
            RegenerateMainConfigFile();
            reloadBindServer();
        }

        public void UpdateZone(DnsZone dnsZone)
        {
            regenerateZoneConfigFile(dnsZone);
            RegenerateMainConfigFile();
            reloadBindServer();
        }

        public void DeleteZone(DnsZone dnsZone)
        {
            deleteZoneConfigFile(dnsZone);
            RegenerateMainConfigFile();
            reloadBindServer();
        }

        private void reloadBindServer()
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.FileName = getRndcFilePath();
            startInfo.Arguments = "reload";
            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            Process p = Process.Start(startInfo);
            p.WaitForExit();
        }

        private string getRndcFilePath()
        {
            return Path.Combine(ServerConfig.IscBindDirectory.FullName, @"bin\rndc");
        }

        private void deleteZoneConfigFile(DnsZone zone)
        {
            string zoneConfigFilePath = getZoneConfigPath(getZonesDirectory().FullName, zone);
            FileInfo zoneConfigFile = new FileInfo(zoneConfigFilePath);
            zoneConfigFile.Delete();
        }

        private FileInfo getMainConfigFile()
        {
            return new FileInfo(Path.Combine(ServerConfig.IscBindDirectory.FullName, @"etc\named.generated.conf"));
        }

        private DirectoryInfo getZonesDirectory()
        {
            return new DirectoryInfo(Path.Combine(ServerConfig.IscBindDirectory.FullName, @"etc\" + zoneConfigFileDirectory));
        }

        private void regenerateZoneConfigFile(DnsZone zone)
        {
            // After first installation, zones directory may not exist.
            DirectoryInfo zonesDirectory = getZonesDirectory();
            if (!zonesDirectory.Exists)
            {
                zonesDirectory.Create();
            }

            FileStream stream = File.Open(
                getZoneConfigPath(zonesDirectory.FullName, zone),
                FileMode.Create,
                FileAccess.ReadWrite);

            StreamWriter writer = new StreamWriter(stream);
            writer.Write(getZoneConfigContent(zone));
            writer.Flush();

            stream.Close();
        }

        private string getZoneConfigPath(string directoryPath, DnsZone zone)
        {
            return Path.Combine(directoryPath, string.Format(zoneConfigFileNameFormat, zone.Name));
        }

        public void RegenerateMainConfigFile()
        {
            FileInfo configFile = getMainConfigFile();

            FileStream stream = File.Open(
                configFile.FullName,
                FileMode.Create,
                FileAccess.ReadWrite);

            StreamWriter writer = new StreamWriter(stream);
            writer.Write(getMainConfigFileContent());
            writer.Flush();

            stream.Close();
        }

        private string getMainConfigFileContent()
        {
            StringBuilder builder = new StringBuilder();
            builder.AppendFormat("#\r\n# {0}\r\n#\r\n", generatedConfigFileComment);

            // For each zone, add an index to the zone file.
            HostingConfig.GetArray<DnsZone>().ToList().ForEach(
                z => builder.AppendLine(getZoneConfigIndex(z)));

            return builder.ToString();
        }

        private string getZoneConfigIndex(DnsZone z)
        {
            return string.Format(
                "zone \"{0}\" IN {{ type master; file \"{1}\"; allow-transfer {{ none; }}; }};",
                z.Name,
                getZoneConfigPath(zoneConfigFileDirectory, z));
        }

        private string getZoneConfigContent(DnsZone zone)
        {
            if (string.IsNullOrEmpty(zone.Name))
            {
                throw new Exception("The DNS zone name cannot be empty.");
            }

            if (string.IsNullOrEmpty(zone.DefaultTtl))
            {
                throw new Exception("The DNS zone default TTL cannot be empty.");
            }

            StringBuilder builder = new StringBuilder();

            builder.AppendFormat(";\r\n; {0}\r\n;\r\n", generatedConfigFileComment);
            builder.AppendFormat("$TTL {0}\r\n", zone.DefaultTtl);

            builder.AppendFormat(
                "@ IN SOA {0} {1} ({2}{3} {4} {5} {6} {7})",
                ServerConfig.FirstDnsNameServer,
                ServerConfig.DnsHostmasterName,
                DateTime.Today.ToString("yyyyMMdd"),
                "00", // TODO: Implement revision increment.
                TimeSpan.FromHours(1).TotalSeconds, // Refesh
                TimeSpan.FromMinutes(10).TotalSeconds, // Retry
                TimeSpan.FromDays(14).TotalSeconds, // Expire
                TimeSpan.FromHours(1).TotalSeconds // Minimum
            );
            builder.AppendLine();

            if (zone.RecordArray.Count(r => r.RecordType == DnsRecordType.NS) == 0)
            {
                throw new InvalidOperationException(
                    "Record array must contain at least one NS record.");
            }

            // Get zones that aren't due to be deleted.
            var rq = from r in zone.RecordArray
                     where r.PendingAction != ChildPendingAction.Delete
                     select r;

            // Append a config string for each record to the builder.
            rq.ToList().ForEach(r => builder.AppendLine(getRecordConfig(r)));

            return builder.ToString();
        }

        private string getRecordConfig(DnsRecord r)
        {
            if (string.IsNullOrEmpty(r.Name))
            {
                throw new NotSupportedException(
                    "Empty record names are not supported. If record name is " +
                    "meant to be the same as zone, then use the @ character.");
            }

            if (string.IsNullOrEmpty(r.Value))
            {
                throw new InvalidOperationException(
                    "Cannot get record config for a record with no value.");
            }

            List<string> partList = new List<string>();

            // Add trim for tidyness.
            partList.Add(r.Name.Trim());

            // Only add TTL if it exists.
            if (!string.IsNullOrEmpty(r.Ttl))
            {
                partList.Add(r.Ttl.Trim());
            }

            // Just use the string representation.
            partList.Add(r.RecordType.ToString());

            // Add trim for tidyness.
            partList.Add(r.Value.Trim());

            return string.Join(" ", partList.ToArray());
        }
    }
}
